package com.nulldev.util.data.Base;

import java.nio.charset.Charset;

import com.nulldev.util.VariableAPI.StringsUtil;
import com.nulldev.util.data.Charsets.CharsetUtil;

public abstract class BaseNCodec implements BinaryEncoder, BinaryDecoder {
	static final int EOF = -1;
	public static final int MIME_CHUNK_SIZE = 76;
	public static final int PEM_CHUNK_SIZE = 64;
	@SuppressWarnings("unused")
	private static final int DEFAULT_BUFFER_RESIZE_FACTOR = 2;
	private static final int DEFAULT_BUFFER_SIZE = 8192;
	protected static final int MASK_8BITS = 255;
	protected static final byte PAD_DEFAULT = 61;
	@Deprecated
	protected final byte PAD = 61;
	protected final byte pad;
	private final int unencodedBlockSize;
	private final int encodedBlockSize;
	protected final int lineLength;
	private final int chunkSeparatorLength;

	protected BaseNCodec(final int unencodedBlockSize, final int encodedBlockSize, final int lineLength, final int chunkSeparatorLength) {
		this(unencodedBlockSize, encodedBlockSize, lineLength, chunkSeparatorLength, (byte) 61);
	}

	protected BaseNCodec(final int unencodedBlockSize, final int encodedBlockSize, final int lineLength, final int chunkSeparatorLength, final byte pad) {
		this.unencodedBlockSize = unencodedBlockSize;
		this.encodedBlockSize = encodedBlockSize;
		final boolean useChunking = lineLength > 0 && chunkSeparatorLength > 0;
		this.lineLength = (useChunking ? (lineLength / encodedBlockSize * encodedBlockSize) : 0);
		this.chunkSeparatorLength = chunkSeparatorLength;
		this.pad = pad;
	}

	boolean hasData(final Context context) {
		return context.buffer != null;
	}

	int available(final Context context) {
		return (context.buffer != null) ? (context.getPos() - context.readPos) : 0;
	}

	protected int getDefaultBufferSize() {
		return DEFAULT_BUFFER_SIZE;
	}

	private byte[] resizeBuffer(final Context context) {
		if (context.buffer == null) {
			context.buffer = new byte[this.getDefaultBufferSize()];
			context.setPos(0);
			context.readPos = 0;
		} else {
			final byte[] b = new byte[context.buffer.length * 2];
			System.arraycopy(context.buffer, 0, b, 0, context.buffer.length);
			context.buffer = b;
		}
		return context.buffer;
	}

	protected byte[] ensureBufferSize(final int size, final Context context) {
		if (context.buffer == null || context.buffer.length < context.getPos() + size) {
			return this.resizeBuffer(context);
		}
		return context.buffer;
	}

	int readResults(final byte[] b, final int bPos, final int bAvail, final Context context) {
		if (context.buffer != null) {
			final int len = Math.min(this.available(context), bAvail);
			System.arraycopy(context.buffer, context.readPos, b, bPos, len);
			context.readPos += len;
			if (context.readPos >= context.getPos()) {
				context.buffer = null;
			}
			return len;
		}
		return context.getEof() ? -1 : 0;
	}

	protected static boolean isWhiteSpace(final byte byteToCheck) {
		switch (byteToCheck) {
			case 9:
			case 10:
			case 13:
			case 32: {
				return true;
			}
			default: {
				return false;
			}
		}
	}

	@Override
	public Object encode(final Object obj) throws Exception {
		if (!(obj instanceof byte[])) {
			throw new Exception("Parameter supplied to Base-N encode is not a byte[]");
		}
		return this.encode((byte[]) obj);
	}

	public String encodeToString(final byte[] pArray) {
		return StringsUtil.NewStrings.newStringUtf8(this.encode(pArray));
	}

	public String encodeAsString(final byte[] pArray) {
		return StringsUtil.NewStrings.newStringUtf8(this.encode(pArray));
	}

	@Override
	public Object decode(final Object obj) throws Exception {
		if (obj instanceof byte[]) {
			return this.decode((byte[]) obj);
		}
		if (obj instanceof String) {
			return this.decode((String) obj);
		}
		throw new Exception("Parameter supplied to Base-N decode is not a byte[] or a String");
	}

	public byte[] decode(final String pArray) {
		return this.decode(pArray, CharsetUtil.getCharset());
	}

	public byte[] decode(final String pArray, final Charset charset) {
		return this.decode(StringsUtil.GetBytes.getBytes(pArray, charset));
	}

	@Override
	public byte[] decode(final byte[] pArray) {
		if (pArray == null || pArray.length == 0) {
			return pArray;
		}
		final Context context = new Context();
		this.decode(pArray, 0, pArray.length, context);
		this.decode(pArray, 0, -1, context);
		final byte[] result = new byte[context.getPos()];
		this.readResults(result, 0, result.length, context);
		return result;
	}

	@Override
	public byte[] encode(final byte[] pArray) {
		if (pArray == null || pArray.length == 0) {
			return pArray;
		}
		return this.encode(pArray, 0, pArray.length);
	}

	public byte[] encode(final byte[] pArray, final int offset, final int length) {
		if (pArray == null || pArray.length == 0) {
			return pArray;
		}
		final Context context = new Context();
		this.encode(pArray, offset, length, context);
		this.encode(pArray, offset, -1, context);
		final byte[] buf = new byte[context.getPos() - context.readPos];
		this.readResults(buf, 0, buf.length, context);
		return buf;
	}

	public abstract void encode(final byte[] p0, final int p1, final int p2, final Context p3);

	public abstract void decode(final byte[] p0, final int p1, final int p2, final Context p3);

	protected abstract boolean isInAlphabet(final byte p0);

	public boolean isInAlphabet(final byte[] arrayOctet, final boolean allowWSPad) {
		for (final byte octet : arrayOctet) {
			if (!this.isInAlphabet(octet) && (!allowWSPad || (octet != this.pad && !isWhiteSpace(octet)))) {
				return false;
			}
		}
		return true;
	}

	public boolean isInAlphabet(final String basen) {
		return this.isInAlphabet(StringsUtil.GetBytes.getBytesUtf8(basen), true);
	}

	protected boolean containsAlphabetOrPad(final byte[] arrayOctet) {
		if (arrayOctet == null) {
			return false;
		}
		for (final byte element : arrayOctet) {
			if (this.pad == element || this.isInAlphabet(element)) {
				return true;
			}
		}
		return false;
	}

	public long getEncodedLength(final byte[] pArray) {
		long len = (pArray.length + this.unencodedBlockSize - 1) / this.unencodedBlockSize * this.encodedBlockSize;
		if (this.lineLength > 0) {
			len += (len + this.lineLength - 1L) / this.lineLength * this.chunkSeparatorLength;
		}
		return len;
	}
}
